/*******************************************************************************
 * Copyright (c) 2014 Francesco Gabbrielli
 * 
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v2.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * 
 * Contributors:
 *     Francesco Gabbrielli - initial API and implementation
 ******************************************************************************/
package it.francescogabbrielli.android.solidalapp;

import it.francescogabbrielli.android.solidalapp.NetworkUtils.Status;
import it.francescogabbrielli.android.solidalapp.StatusEvent.Type;
import it.francescogabbrielli.android.solidalapp.actions.EndpointsGetDevice;
import it.francescogabbrielli.android.solidalapp.actions.EndpointsGetProfile;
import it.francescogabbrielli.android.solidalapp.actions.EndpointsUpdateDevice;
import it.francescogabbrielli.android.solidalapp.actions.GCMRegisterDevice;
import it.francescogabbrielli.android.solidalapp.deviceendpoint.model.Device;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.location.Location;
import android.os.Handler;
import android.util.Log;
import android.widget.Toast;

/**
 * Application with embedded status using subclass pattern
 * 
 * Contains a reference to the current activity, the status 
 * of the connection to the various services, and the application data.
 * 
 * @author Francesco Gabbrielli
 */
public class SolidalApp extends Application {
	
	public final static String TAG = "SolidalApp";
	
	public enum DataKey {
		ADDRESS_LOCATION_MANUAL, ADDRESS_TEXT_MANUAL,
		ADDRESS_LOCATION, ADDRESS_TEXT,
		PROFILE, DEVICE, REQUEST, RESPONSE,
		FILE_RESPONSE, FILE_RESPONSE_PROFILE,
		FILE_REQUEST, FILE_REQUEST_PROFILE,
		CHAT_REQUEST, CHAT_RESPONSE
		;
	}
	
	/** The current activity */
	private Activity currentActivity;
	
	/** Connection status */
	private AppStatus status;

	/** Application (temporary) data */
	private Map<DataKey, Object> data;
	
	private RetryManager retryManager;
	
	public synchronized Activity getCurrentActivity() {
//		return !stack.isEmpty() ? stack.peek().activity : null;
		return currentActivity;
	}

	public synchronized void setCurrentActivity(Activity activity) {
		// do not wait for previous activity to destroy
		if (currentActivity!=null) {
			if (currentActivity instanceof StatusListener)
				removeStatusListener((StatusListener) currentActivity);
			if (currentActivity instanceof DataListener)
				removeDataListener((DataListener) currentActivity);
		}
		currentActivity = activity;
//		stack.push(new StackElement(activity));
	}
	
	public void removeCurrentActivity(final Activity activity) {
		// delay the setting until activity thread completed
		new Handler(getMainLooper()).post(new Runnable() {
			@Override
			public void run() {
				synchronized (SolidalApp.this) {
					if (activity==currentActivity)
						currentActivity = null;
				}
			}
		});
	}
	
	public AppStatus getStatus() {
		return status;
	}
	
	@Override
	public void onConfigurationChanged(Configuration newConfig) {
		Log.i(SolidalApp.TAG, "CONFIG: "+newConfig);
		super.onConfigurationChanged(newConfig);
	}
			
	@Override
	public void onCreate() {
		
		super.onCreate();
		
		status = new AppStatus(this);
		data = new HashMap<SolidalApp.DataKey, Object>();
		
		SharedPreferences prefs = getSharedPreferences(getClass().getCanonicalName(), MODE_PRIVATE);
		SharedPreferences.Editor editor = prefs.edit();
//		editor.clear();
		editor.putInt(Constants.INTENT, 0);
		editor.putInt(Constants.NOTIFICATION, 0);
		editor.commit();
	}
	
	/**
	 * Check registration to GCM and Endpoints (needed for receiving requests)
	 */
	public void registerToServices() {
		if(status.getNetworkStatus()==Status.CONNECTED) {
			Log.d(TAG, "Check registration");
			if(getData(DataKey.PROFILE)==null) {
				Action.start(this, EndpointsGetProfile.class);
			} else if(status.getGcmId()==null) {
				Action.start(this, GCMRegisterDevice.class);
			} else if(getData(DataKey.DEVICE)==null) {
				Action.start(this, EndpointsGetDevice.class);
			}
		} else {
			
		}
	}
	
	public RetryManager getRetryManager() {
		if (retryManager==null)
			retryManager = new RetryManager(this);
		return retryManager;
	}
	
	private List<StatusListener> statusListeners;
	
	public synchronized List<StatusListener> getStatusListeners() {
		if(statusListeners==null)
			statusListeners = new LinkedList<StatusListener>();
		return statusListeners;
	}
	
	public synchronized boolean addStatusListener(StatusListener listener) {
		if(!getStatusListeners().contains(listener)) {
			getStatusListeners().add(listener);
			return true;
		}
		return false;
	}
	
	public synchronized void removeStatusListener(StatusListener listener) {
		getStatusListeners().remove(listener);
	}
	
	synchronized void fireStatusUpdate(StatusEvent event) {
		for(StatusListener l : getStatusListeners())
			l.onStatusUpdate(event);
		
		// network retries
		if (event.getType()==Type.NETWORK && status.getNetworkStatus()==Status.CONNECTED) {
			registerToServices();
			getRetryManager().retry();
		}
	}
	
	public boolean addNetworkRetry(RetryListener listener) {
		if (listener!=null)
			return getRetryManager().add(listener);
		return false;
	}
	
	public void removeNetworkRetry(RetryListener listener) {
		if (listener!=null)
			getRetryManager().remove(listener);
	}
	

	private List<DataListener> dataListeners;
	
	public synchronized List<DataListener> getDataListeners() {
		if(dataListeners==null)
			dataListeners = new LinkedList<DataListener>();
		return dataListeners;
	}
	
	public synchronized Object getData(DataKey key) {
		return data.get(key);
	}
	
	public synchronized boolean setData(DataKey key, Object value) {
		return setData(key, value, null);
	}
	
	public synchronized boolean setData(DataKey key, Object value, Boolean forceRefire) {
//		if((value==null && getData(key)!=null) || (value!=null && !value.equals(getData(key)))) {
		boolean force = forceRefire!=null && forceRefire.booleanValue();
		if (force || value!=getData(key)) {//simpler...
			data.put(key, value);
			Log.v(TAG, "Set data: "+key+"="+value);
			if (force || forceRefire==null)
				fireDataChanged(new DataEvent(this, key, true));
			return true;
		}
		return false;
	}
		
	public synchronized boolean addDataListener(DataListener listener) {
		if(!getDataListeners().contains(listener)) {
			getDataListeners().add(listener);
			return true;
		}
		return false;
	}
	
	public synchronized void removeDataListener(DataListener listener) {
		getDataListeners().remove(listener);
	}
	
	public synchronized void fireDataChanged(DataEvent event) {
		
		// auto-listen...
		if (event.isTriggered()) {
			if (event.getKey()==DataKey.ADDRESS_LOCATION)
				trackLocation((Location) getData(DataKey.ADDRESS_LOCATION));
			if (event.getKey()==DataKey.ADDRESS_LOCATION_MANUAL && getData(DataKey.ADDRESS_LOCATION_MANUAL)==null)
				setData(DataKey.ADDRESS_LOCATION, status.getLocation());
		}
		
		// fire with try-catch
		for(DataListener l : getDataListeners())
			try {
				l.onDataChanged(event);
			} catch(Throwable e) {
				Log.e(TAG, "Cannot fire data "+event, e);
			}
	}
	
	public final static long UPDATE_DEVICE_TIME = 12 * 60 * 60 * 1000l;
	
	private void trackLocation(Location location) {
		// check preferences
 		SharedPreferences prefs = getSharedPreferences(
 				SolidalApp.class.getCanonicalName(), Context.MODE_PRIVATE);
		if ("NONE".equals(prefs.getString(Constants.PREF_NOTIFY, "NONE")))
			return;
		
		// check curent device
 		Device currentDevice = (Device) getData(DataKey.DEVICE);
		if (currentDevice!=null) {
			boolean upToDate = isUpToDate();
			if (location!=null && upToDate) {
				double diffY = Math.abs(location.getLatitude() - currentDevice.getLatitude());
				double diffX = Math.abs(location.getLongitude() - currentDevice.getLongitude());
				upToDate = diffX * 122d < 0.125d && diffY * 122d < 0.125d;
				Log.v(TAG, "Location changed by "+diffX+","+diffY+": "+upToDate);
			}
			if (!upToDate)
				Action.start(this, EndpointsUpdateDevice.class, currentDevice);
		}
	}
			
	public void alert(int messageId) {
		alert(messageId, false);
	}
	
	public void alert(int messageId, boolean forced) {
		alert(getString(messageId), forced);
	}
	
	public void alert(String message) {
		alert(message, false);
	}
	
	public void alert(String message, boolean forced) {
		Activity currentActivity = getCurrentActivity();
		if (currentActivity!=null || forced)
			Toast.makeText(
					currentActivity!=null ? currentActivity : this, 
					message!=null ? message : "?",
					forced ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show();
	}
	
	public boolean isUpToDate() {
		boolean upToDate = true;
		Device device = (Device) getData(DataKey.DEVICE);
		if (device!=null) {
			int version = 0;
			try {
				version = getPackageManager().getPackageInfo(getPackageName(), 0).versionCode;
			} catch(Exception e) {
				Log.e(SolidalApp.TAG, "Unknown version?", e);
			}
			if (System.currentTimeMillis() - device.getTimestamp() > UPDATE_DEVICE_TIME)
				upToDate = false;
			else if (device.getAppVersion() < version)
				upToDate = false;
		}
		return upToDate;
	}
	
	private Map<Class<? extends Action>, Action> actions;
	
	private final static long ACTION_TIMEOUT = 30000;
	
	private Map<Class<? extends Action>, Action> getActions() {
		if (actions==null)
			actions = new HashMap<Class<? extends Action>, Action>();
		return actions;
	}
	
	public synchronized Action getAction(Class<? extends Action> actionClass) {
		return getActions().get(actionClass);
	}
	
	public synchronized Action putAction(Class<? extends Action> actionClass) throws Exception {
		Action old = getActions().get(actionClass);
		if (old==null || old.getTimestamp() < System.currentTimeMillis() - ACTION_TIMEOUT) {
			Action ret = actionClass.newInstance();
			getActions().put(actionClass, ret);
			return ret;
		}
		return null;
	}
	
	public synchronized Action removeAction(Class<? extends Action> actionClass) {
		return getActions().remove(actionClass);
	}
	
}
